/*
 *  Copyright 2004 Blandware (http://www.blandware.com)
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package com.blandware.atleap.webapp.action.core;

import com.blandware.atleap.common.Constants;
import com.blandware.atleap.webapp.util.core.ApplicationResources;
import com.blandware.atleap.webapp.util.core.SslUtil;
import com.blandware.atleap.webapp.util.core.TokenUtil;
import com.blandware.atleap.webapp.util.core.WebappConstants;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.struts.Globals;
import org.apache.struts.action.Action;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionMapping;
import org.apache.struts.action.ActionMessages;
import org.springframework.context.ApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.HashMap;


/**
 * <p>Implementation of <strong>Action</strong> that contains base methods for
 * logging and conducting pre/post perform actions. This class is intended to
 * be a base class for all Struts actions.
 * </p>
 * <p>You can create action-forward with name "unsatisfiable" (it is reserved name) in order to check for token before form validation.
 * The token will be reseted after checking.
 * </p>
 * <p><a href="BaseAction.java.html"><i>View Source</i></a>
 * </p>
 *
 * @author Matt Raible <a href="mailto:matt@raibledesigns.com">&lt;matt@raibledesigns.com&gt;</a>
 * @author Andrey Grebnev <a href="mailto:andrey.grebnev@blandware.com">&lt;andrey.grebnev@blandware.com&gt;</a>
 * @author Sergey Zubtsovskiy <a href="mailto:sergey.zubtsovskiy@blandware.com">&lt;sergey.zubtsovskiy@blandware.com&gt;</a>
 * @version $Revision: 1.21 $ $Date: 2006/08/03 10:07:45 $
 */
public class BaseAction extends Action {

    protected transient final Log log = LogFactory.getLog(BaseAction.class);
    private static final String SECURE = "secure";
    private ApplicationContext ctx = null;

    /**
     * Gets bean from Spring application context by its name
     *
     * @param name Name of bean to lookup
     * @return Spring bean
     */
    protected Object getBean(String name) {
        if ( ctx == null ) {
            ctx = WebApplicationContextUtils.getRequiredWebApplicationContext(servlet.getServletContext());
        }
        return ctx.getBean(name);
    }

    /**
     * Convenience method to get action messages
     *
     * @param request the current request
     * @return the populated (or empty) messages
     */
    protected ActionMessages getMessages(HttpServletRequest request) {
        ActionMessages messages = null;
        HttpSession session = request.getSession();

        if ( request.getAttribute(Globals.MESSAGE_KEY) != null ) {
            messages =
                    (ActionMessages) request.getAttribute(Globals.MESSAGE_KEY);
            saveMessages(request, messages);
        } else if ( session.getAttribute(Globals.MESSAGE_KEY) != null ) {
            messages =
                    (ActionMessages) session.getAttribute(Globals.MESSAGE_KEY);
            saveMessages(request, messages);
            session.removeAttribute(Globals.MESSAGE_KEY);
        } else {
            messages = new ActionMessages();
        }
        return messages;
    }

    /**
     * Saves ActionMessages object which represents errors in request and session. It is done in order to display
     * action errors even after redirect using our special ErrorsTag.
     *
     * @param request Request to save errors in (they will be saved in session too)
     * @param errors  ActionMessages object to save in request
     * @see com.blandware.atleap.webapp.taglib.core.html.MessagesTag
     */
    protected void saveErrors(HttpServletRequest request, ActionMessages errors) {
        saveErrors(request, errors, true);
    }


    /**
     * Saves ActionMessages object which represents errors in request and session. It is done in order to display
     * action errors even after redirect using our special ErrorsTag.
     *
     * @param request       Request to save errors in (they will be saved in session too)
     * @param errors        ActionMessages object to save in request
     * @param saveToSession Whether or not to save in session
     * @see com.blandware.atleap.webapp.taglib.core.html.MessagesTag
     */
    protected void saveErrors(HttpServletRequest request, ActionMessages errors, boolean saveToSession) {
        HttpSession session = request.getSession();
        if ( errors == null || errors.isEmpty() ) {
            request.removeAttribute(Globals.ERROR_KEY);
            session.removeAttribute(WebappConstants.ERROR_KEY);
        } else {
            request.setAttribute(Globals.ERROR_KEY, errors);
            if ( saveToSession ) {
                session.setAttribute(WebappConstants.ERROR_KEY, errors);
            }
        }
    }

    /**
     * Saves ActionMessages object which represents messages in request and session. It is done in order to display
     * action messages even after redirect using our special MessagesTag.
     *
     * @param request  Request to save messages in (they will be saved in session too)
     * @param messages ActionMessages object to save in request
     * @see com.blandware.atleap.webapp.taglib.core.html.MessagesTag
     */
    protected void saveMessages(HttpServletRequest request, ActionMessages messages) {
        HttpSession session = request.getSession();
        if ( messages == null || messages.isEmpty() ) {
            request.removeAttribute(Globals.MESSAGE_KEY);
            session.removeAttribute(WebappConstants.MESSAGE_KEY);
        } else {
            request.setAttribute(Globals.MESSAGE_KEY, messages);
            session.setAttribute(WebappConstants.MESSAGE_KEY, messages);
        }
    }

    /**
     * Convenience method for getting an action form base on it's mapped scope.
     *
     * @param mapping The ActionMapping used to select this instance
     * @param request The HTTP request we are processing
     * @return ActionForm the form from the specifies scope, or null if nothing
     *         found
     */
    protected ActionForm getActionForm(ActionMapping mapping,
                                       HttpServletRequest request) {
        ActionForm actionForm = null;

        // Remove the obsolete form bean
        if ( mapping.getAttribute() != null ) {
            if ( "request".equals(mapping.getScope()) ) {
                actionForm =
                        (ActionForm) request.getAttribute(mapping.getAttribute());
            } else {
                HttpSession session = request.getSession();

                actionForm =
                        (ActionForm) session.getAttribute(mapping.getAttribute());
            }
        }
        return actionForm;
    }

    /**
     * Convenience method to get the Configuration HashMap
     * from the servlet context.
     *
     * @return the user's populated form from the session
     */
    protected HashMap getConfiguration() {
        return (HashMap) this.getServlet().getServletContext().getAttribute(Constants.CONFIG);
    }

    // --------------------------------------------------------- Public Methods
    // Don't use class variables in Action objects.  These are not session safe.

    /**
     * Method to check and see if https is required for this resource
     *
     * @param mapping  The ActionMapping used to select this instance
     * @param request  The HTTP request we are processing
     * @param response The HTTP response we are creating
     * @return boolean true if redirection to SSL is needed
     */
    protected boolean checkSsl(ActionMapping mapping,
                               HttpServletRequest request,
                               HttpServletResponse response) {
        String redirectString =
                SslUtil.getRedirectString(request,
                        getServlet().getServletContext(),
                        SECURE.equals(mapping.getParameter()));

        if ( redirectString != null ) {
            log.debug("protocol switch needed, redirecting...");

            try {
                // Redirect the page to the desired URL
                response.sendRedirect(response.encodeRedirectURL(redirectString));

                return true;
            } catch ( Exception e ) {
                log.error("redirect to new protocol failed...");

                // Handle appropriately
            }
        }

        return false;
    }

    /**
     * Convenience method for removing the obsolete form bean.
     *
     * @param mapping The ActionMapping used to select this instance
     * @param request The HTTP request we are processing
     */
    protected void removeFormBean(ActionMapping mapping,
                                  HttpServletRequest request) {
        // Remove the obsolete form bean
        if ( mapping.getAttribute() != null ) {
            if ( "request".equals(mapping.getScope()) ) {
                request.removeAttribute(mapping.getAttribute());
            } else {
                HttpSession session = request.getSession();

                session.removeAttribute(mapping.getAttribute());
            }
        }
    }

    /**
     * Convenience method to update a formBean in it's scope
     *
     * @param mapping The ActionMapping used to select this instance
     * @param request The HTTP request we are processing
     * @param form    The ActionForm
     */
    protected void updateFormBean(ActionMapping mapping,
                                  HttpServletRequest request, ActionForm form) {
        // update the obsolete form bean
        if ( mapping.getAttribute() != null ) {
            if ( "request".equals(mapping.getScope()) ) {
                request.setAttribute(mapping.getAttribute(), form);
            } else {
                HttpSession session = request.getSession();

                session.setAttribute(mapping.getAttribute(), form);
            }
        }
    }

    /**
     * Retrieves localized message for specified keys with no arguments
     *
     * @param request Request to lookup message resources in
     * @param key     Key to search message for
     * @return Message for key
     */
    protected String getMessage(HttpServletRequest request, String key) {
        return ApplicationResources.getInstance(request.getSession().getServletContext()).getMessage(getLocale(request), key);
    }


    /*  ==============================  T O K E N S ===================================== */

    /**
     * <p>Returns <code>true</code> if there is a transaction token stored in
     * the user's current session in all scopes beginning from default, and the
     * value submitted as a request parameter with this action matches it.
     * Returns <code>false</code> under any of the following circumstances:</p>
     * <ul>
     * <li>No session associated with this request</li>
     * <li>No transaction token saved in the session</li>
     * <li>No transaction token included as a request parameter</li>
     * <li>The included transaction token value does not match the
     * transaction token in the user's session</li>
     * </ul>
     *
     * @param request The servlet request we are processing
     * @param reset   Should we reset the token after checking it?
     * @return is token existing and valid
     */
    public synchronized boolean isTokenValid(HttpServletRequest request, boolean reset) {
        return TokenUtil.getInstance().isTokenValid(request, reset);
    }

    /**
     * <p>Returns <code>true</code> if there is a transaction token stored in
     * the user's current session in all scopes beginning from default, and the
     * value submitted as a request parameter with this action matches it.
     * Returns <code>false</code> under any of the following circumstances:</p>
     * <ul>
     * <li>No session associated with this request</li>
     * <li>No transaction token saved in the session</li>
     * <li>No transaction token included as a request parameter</li>
     * <li>The included transaction token value does not match the
     * transaction token in the user's session</li>
     * </ul>
     *
     * @param request The servlet request we are processing
     * @return is token existing and valid
     */
    public synchronized boolean isTokenValid(HttpServletRequest request) {
        return TokenUtil.getInstance().isTokenValid(request);
    }


    /**
     * <p>Returns <code>true</code> if there is a transaction token stored in
     * the user's current session in specified scopes, and the value submitted
     * as a request parameter with this action matches it. Returns
     * <code>false</code> under any of the following circumstances:</p>
     * <ul>
     * <li>No session associated with this request</li>
     * <li>No transaction token saved in the session</li>
     * <li>No transaction token included as a request parameter</li>
     * <li>The included transaction token value does not match the
     * transaction token in the user's session</li>
     * </ul>
     *
     * @param scopes  array of scope names, if null check all scopes begin from
     * default
     * @param request The servlet request we are processing
     * @return is token existing and valid
     */
    public synchronized boolean isTokenValid(String[] scopes, HttpServletRequest request) {
        return TokenUtil.getInstance().isTokenValid(scopes, request);
    }

    /**
     * <p>Returns <code>true</code> if there is a transaction token stored in
     * the user's current session in specified scopes, and the value submitted
     * as a request parameter with this action matches it. Returns
     * <code>false</code> if:</p>
     * <ul>
     * <li>No session associated with this request</li>
     * <li>No transaction token saved in the session</li>
     * <li>No transaction token included as a request parameter</li>
     * <li>The included transaction token value does not match the
     * transaction token in the user's session</li>
     * </ul>
     *
     * @param scopes  array of scope names, if null check all scopes begin from
     * default
     * @param request The servlet request we are processing
     * @param reset   Should we reset the token after checking it?
     * @return is token existing and valid
     */
    public synchronized boolean isTokenValid(String[] scopes, HttpServletRequest request, boolean reset) {
        return TokenUtil.getInstance().isTokenValid(scopes, request, reset);
    }

    /**
     * <p>Resets the saved transaction token in the user's session. This
     * indicates that transactional token checking will not be needed
     * on the next request that is submitted.</p>
     *
     * @param tokenScope name of token scope
     * @param request    The servlet request we are processing
     */
    public synchronized void resetToken(String tokenScope, HttpServletRequest request) {
        TokenUtil.getInstance().resetToken(tokenScope, request);
    }

    /**
     * <p>Resets the saved transaction token in the user's session. This
     * indicates that transactional token checking will not be needed
     * on the next request that is submitted.</p>
     *
     * @param request The servlet request we are processing
     */
    public synchronized void resetToken(HttpServletRequest request) {
        TokenUtil.getInstance().resetToken(request);
    }

    /**
     * <p>Saves a new transaction token in the user's current session, creating
     * a new session if necessary.</p>
     *
     * @param tokenScope name of token scope, if null uses default
     * @param request    The servlet request we are processing
     */
    public synchronized void saveToken(String tokenScope, HttpServletRequest request) {
        TokenUtil.getInstance().saveToken(tokenScope, request);
    }

    /**
     * <p>Saves a new transaction token in the user's current session, creating
     * a new session if necessary.</p>
     *
     * @param request The servlet request we are processing
     */
    public synchronized void saveToken(HttpServletRequest request) {
        TokenUtil.getInstance().saveToken(request);
    }

    /**
     * <p>Returns <code>true</code> if the current form's 'back' button was
     * pressed.  This method will check if the <code>WebappConstants.BACK_KEY</code>
     * request attribute has been set, which normally occurs if the 'back'
     * button generated by <strong>CancelTag</strong> of type 'back' was pressed by the user
     * in the current request.  If <code>true</code>, validation performed
     * by an <strong>ActionForm</strong>'s <code>validate()</code> method
     * will have been skipped by the controller servlet.</p>
     *
     * @param request The servlet request we are processing
     * @return whether 'back' button was pressed
     * @see org.apache.struts.taglib.html.CancelTag
     */
    public boolean isBackPressed(HttpServletRequest request) {
        return (request.getAttribute(WebappConstants.BACK_KEY) != null);
    }
}
